#!/usr/bin/python

#Imports
import os
import sys
import ConfigParser
from time import localtime, strftime
import shutil
import copy
from functions import count_total_bytes

import pygtk

#Command line argument variables
debug = 0 #Prints debugging information when equal to 1
debug_huge = 0 #Prints ALOT of debugging information when equal to 1
simulate = 0 #Disables writing to dics when equal to 1
do_not_follow_links_files = 0 #Disables following links to files
do_not_follow_links_dir = 0 #Disables following links to directories

#Variables
hardlinks = 0 #Counter for hard-links
copies_changed = 0 #Counter for copies (changed)
copies_new = 0 #Counter for copies (new)
files_with_errors = 0 #Counter for files with errors
errors = [] #Stores all errors
fileerror = 0 #Tells is there where an error while copying/creating hard-link
ignore_folders = [] #Folders to be ignored
#files_total = 0 #The total number of files
#files_counter = 0 #The counter, to be compared with files_total
files_total_bytes = 0 #The total size, of all files
files_counter_bytes = 0 #The counter, to be compared with files_total_bytes
percentage_done = 0 #Calculate how much done, in percent
folder = "" #The variable used to store the current folder

#Constants
maxBakLen_Constant = 20 #The number of previous backup folders, in the config file
Version = "0.2.6"

################################################################################
########################## READ COMMAND LINE ARGUMENTS #########################
################################################################################

args = sys.argv
while len(args) > 1 :
	arg = args.pop(1)
	if arg == "-d" or arg == "--debug" :
		print "Running with the show debug information on"
		debug = 1
	elif arg == "--debug-huge" :
		print "Running with the show ALOT of debug information on"
		debug = 1
		debug_huge = 1
	elif arg == "-h" or arg == "--help" or arg == "-v" or arg == "--version" :
		print "Usage: backup.py [FLAGS] PATH"
		print ""
		print "The script accepts the following arguments:"
		print ""
		print "Information flags:"
		print "  -h, --help                        Displays this help text."
		print "  -s, --simulate                    Run in simulation mode."
		print "  -d, --debug                       Display debug information."
		print "      --debug-huge                  Display ALOT of debug information."
		print ""
		print "Link flags:"
		print "  -l, --do-not-follow-links         Doesn't follow any links."
		print "      --do-not-follow-links-dir     Doesn't follow links to directories."
		print ""
		print "If PATH isn't set the default PATH /home/ is used"
		print ""
		print "Linux hard backup tool by Jonas Hansen. Current version: " + Version
		print "http://code.google.com/p/linux-hard-backup-tool/"
		print ""
		exit(0)
	elif arg == "-s" or arg == "--simulate" :
		print "Running in SIMULATION mode: No changes will be made to the disc"
		simulate = 1
	elif arg == "-l" or arg == "--do-not-follow-links" :
		do_not_follow_links_files = 1
		do_not_follow_links_dir = 1
	elif arg == "--do-not-follow-links-dir" :
		do_not_follow_links_dir = 1
	elif not arg[0] == "-" :
		folder = arg
	else :
		print "ERROR: Unknown flag: " + arg
		exit(1)

################################################################################
############################## PREPARE VARIABLES ###############################
################################################################################

if folder == "" : #No PATH was set in arguments, use default PATH
	folder = "/home/"

folderNow = strftime("%Y-%m-%d %H:%M:%S", localtime()) #Name of the new backup folder

if folder[-1] != "/" : #Check for ending '/'
	folder = folder + "/" #Add an ending '/'
folderOrg = folder #Store the current folder for later use

if debug_huge :
	print "Source directory is: " + folder

################################################################################
############################### READ CONFIG FILE ###############################
################################################################################

config = ConfigParser.RawConfigParser() #Prepare config object

if os.path.exists(folderOrg + ".backup") : #Checking for configuration file
	if debug_huge :
		print "Config file found in: " + folderOrg + ".backup"
	config.read(folderOrg + ".backup") #Read from folder-to-backups config file
	if debug_huge :
		print "Read config file: " + folderOrg + ".backup"
	if config.has_option("backup", "folderBack") :
		folderBack = config.get("backup", "folderBack")
		if folderBack[-1] != "/" :
			folderBack = folderBack + "/"  #Add an ending '/' if it's not already there
	else :
		#WORK NEEDED! Show some kind of error
		print "ERROR: Unable to find backup directory!"
		exit(1)
	if config.has_option("backup", "maxBakLen") :
		maxBakLen = int(config.get("backup", "maxBakLen"))
	else :
		maxBakLen = maxBakLen_Constant
	if config.has_option("backup", "folderLastCount") :
		folderLastCount = config.get("backup", "folderLastCount")
	else :
		folderLastCount = maxBakLen
	if config.has_option("backup", "recursive") :
		recursive = config.get("backup", "recursive")
	else :
		recursive = 0
	if not do_not_follow_links_dir :
		if config.has_option("backup", "do_not_follow_links_dir") :
			if config.get("backup", "do_not_follow_links_dir") == "1" :
				do_not_follow_links_dir = 1
			else :
				do_not_follow_links_dir = 0
	if not do_not_follow_links_files :
		if config.has_option("backup", "do_not_follow_links_files") :
			if config.get("backup", "do_not_follow_links_files") == "1" :
				do_not_follow_links_files = 1
			else :
				do_not_follow_links_files = 0
	if config.has_option("backup", "ignore") :
		ignoreCount = config.get("backup", "ignore")
		for n in range(1, int(ignoreCount) + 1) :
			if config.has_option("backup", "ignore" + str(n)) :
				tmp = config.get("backup", "ignore" + str(n))
				if tmp[-1] != "/" :
					tmp = tmp + "/"
				ignore_folders.append(folderOrg + tmp)

################################################################################
##################### FIND LAST (AND USABLE) BACKUP FOLDER #####################
################################################################################

	folderLast = ""
	if os.path.exists(folderBack + "latest/.backup") : #Checking if the "latest" exists
		folderLast = "latest"
		folderLastCount = 1
	else :
		if folderLastCount != 0 : #Last folder most likely exists
			if config.has_option("backup", "folderLast" + str(folderLastCount)) : #Checking for the last used backup folder
				if os.path.exists(folderBack + config.get("backup", "folderLast" + str(folderLastCount)) + "/.backup") :
					folderLast = config.get("backup", "folderLast" + str(folderLastCount))
				else :
					for n in range(1, maxBakLen + 1) :
						if config.has_option("backup", "folderLast" + str(n)) :
							if os.path.exists(folderBack + config.get("backup", "folderLast" + str(n)) + "/.backup") :
								folderLast = config.get("backup", "folderLast" + str(n))
								break #Folder found
						elif n == maxBakLen :
							#Folder NOT found
							folderLastCount = 0

################################################################################
################################ CREATE CONFIG #################################
################################################################################

else : #Configation file doesn't exist
	#WORK NEEDED! Configuration wizzard...
	config.add_section("backup")
	config.set("backup", "folderBack", "")
	config.set("backup", "folderLastCount", 0)
	for n in range(1, maxBakLen_Constant + 1) :
		config.set("backup", "folderLast" + str(n), "")

	folderLastCount = 0
	folderLast = ""
	folderBack = "" #WORK NEEDED: This can't be right!

	if folderBack[-1] != "/" : #Check for ending '/'
		folderBack = folderBack + "/" #Add an ending '/'

	print "ERROR: PLEASE CREATE CONFIG FILE!"
	exit(1)

################################################################################
################################ PREPARE LISTS #################################
################################################################################

if debug :
	print "Last folder is: " + folderBack + folderLast

folderBackOrg = folderBack
folders = [folder]
foldersBack = [folderBack + folderNow + "/"]
foldersBackMode = [folderBack + folderNow + "/"]
foldersLast =  [folderBack + folderLast + "/"]

################################################################################
############################### COUNT ALL FILES ################################
################################################################################

ignore_folders_org = copy.copy(ignore_folders) #Copy the list of folders to be ignonred to an tmp list

if debug :
	print "Counting all files in: " + folder
if debug_huge :
	print "The recursive flag is set to: " + recursive

files_total_bytes = count_total_bytes(folders, recursive, ignore_folders)

folders = [folderOrg]
ignore_folders = ignore_folders_org

################################################################################
############################### BACKUP PROCESS #################################
################################################################################

while len(folders) > 0 : #WORK NEEDED! Show some kind of status...
	folder = folders.pop(0)
	folderBack = foldersBack.pop(0)
	folderLast = foldersLast.pop(0)

	configBak = ConfigParser.RawConfigParser()
	if os.path.exists(folderLast + ".backup") :
		configBak.read(folderLast + ".backup")
		if configBak.has_option("backup", "files") :
			i = int(configBak.get("backup", "files"))
		else :
			i = 0
	else :
		i = 0
	filesBak = []
	filesdateBak = []
	for n in range(1, i + 1) :
		if configBak.has_option("backup", "file" + str(n)) :
			filesBak.append(configBak.get("backup", "file" + str(n)))
			if configBak.has_option("backup", "filedate" + str(n)) :
				filesdateBak.append(configBak.get("backup", "filedate" + str(n)))
			else :
				filesdateBak.append("")

	configBakNew = ConfigParser.RawConfigParser() #Creating the new config file
	configBakNew.add_section("backup")
	if not simulate :
		if debug_huge :
			print "Creating backup directory: " + folderBack
		fileerror = 1
		try :
			os.makedirs(folderBack)
			fileerror = 0
		except (IOError, os.error), why : #catch the Error
			errors.append((folderBack, folderBack, str(why)))
		except Error, err :
			errors.extend(err.args[0])
		except :
			print "Unexpected error:", sys.exc_info()[0]
		if fileerror :
			print "ERROR: Unable to create backup directory"
			exit(1)

	else :
		print "Creating backup directory: " + folderBack
	n = 0
	fileerror = 1
	try :
		files=os.listdir(folder)
		fileerror = 0
	except (IOError, os.error), why : #catch the Error
		errors.append((folder + filename, folderBack + filename, str(why)))
	except Error, err :
		errors.extend(err.args[0])
	except :
		print "Unexpected error:", sys.exc_info()[0]
	if fileerror :
		print "ERROR: Unable to list directory '" + folder + "', continuing without it!"
	else :
		if ( simulate or debug ) :
			print ""
			print "Opening directory: " + folder

		for filename in files :
			if filename != ".backup" :
				if os.path.isfile(folder + filename) :
					if not os.path.islink(folder + filename) :
#						files_counter += 1
#						percentage_done = round(files_counter*100.0/files_total,1) #Calculate how much done (just in number of files), in percent
						files_counter_bytes += os.stat(folder + filename).st_size
						percentage_done = round(files_counter_bytes*100.0/files_total_bytes,1) #Calculate how much done (in bytes), in percent
						if not debug :
							print " ", percentage_done, " %\r",
							sys.stdout.flush()
						else :
							print percentage_done
						fileerror = 1 #Setting file error true
						n += 1
						try :
							filedate = os.stat(folder + filename).st_mtime #Read the files last modified date
							filemode = os.stat(folder + filename).st_mode #Read the files permissions
						except (IOError, os.error), why : #catch the Error
							errors.append((folder + filename, folder + filename, str(why)))
						except Error, err :
							errors.extend(err.args[0])
						except :
							print "Unexpected error:", sys.exc_info()[0]

						if folderLastCount != 0 : #Only look for the file if there is a last backup folder
							try :
								i = filesBak.index(filename)
							except ValueError :
								i = -1
						else : #No last backup folder
							i = -1
						if i != -1 :
							if str(filedate) == filesdateBak[i] :
								if debug :
									print "Object hasn't been changed; creating hard-link to previous backup"
									if debug_huge :
										print "Hard-link: " + folderLast + filename + " > " + folderBack + filename
								if not simulate :
									tmpfile = folderLast + filename + folderNow #Creating temporary file
									try :
										os.chmod(folderLast, 511) #Setting permissions on folder to read/write
										os.link(folderLast + filename, tmpfile) #Creating hard-link to temporary file
										shutil.move(tmpfile, folderBack + filename) # Moing temporary file to correct location
										os.chmod(folderLast, 365) #Setting permissions on folder to read-only, for safety reasons
										fileerror = 0 #No file error
									except (IOError, os.error), why : #catch the Error
										errors.append((folderLast + filename, folderBack + filename, str(why)))
									except Error, err :
										errors.extend(err.args[0])
									except :
										print "Unexpected error:", sys.exc_info()[0]
								else :
									print "Hard-link: " + folderLast + filename + " > " + folderBack + filename
								if not fileerror :
									hardlinks += 1

							else :
								if debug :
									print "Object has been changed; copying"
									if debug_huge :
										print "Copy: " + folder + filename + " > " + folderBack + filename
								if not simulate :
									try :
										shutil.copy2(folder + filename, folderBack + filename) #Copying changed file
										fileerror = 0 #No file error
									except (IOError, os.error), why : #catch the Error
										errors.append((folder + filename, folderBack + filename, str(why)))
									except Error, err :
										errors.extend(err.args[0])
									except :
										print "Unexpected error:", sys.exc_info()[0]
								else :
									print "Copy: " + folder + filename + " > " + folderBack + filename
								if not fileerror :
									copies_changed += 1

						else :
							if debug :
								print "Object is a new file; copying"
								if debug_huge :
									print "Copy: " + folder + filename + " > " + folderBack + filename
							if not simulate :
								try :
									shutil.copy2(folder + filename, folderBack + filename) #Copying new file
									fileerror = 0 #No file error
								except (IOError, os.error), why : #catch the Error
									errors.append((folder + filename, folderBack + filename, str(why)))
								except Error, err :
									errors.extend(err.args[0])
								except :
									print "Unexpected error:", sys.exc_info()[0]
							else :
								print "Copy: " + folder + filename + " > " + folderBack + filename
							if not fileerror :
								copies_new += 1
						if not simulate :
							if debug_huge :
								print "Setting file permissions: " + folderBack + filename
							if not fileerror :
								try :
									os.chmod(folderBack + filename, 292) # Setting file permissions
								except (IOError, os.error), why : #catch the Error
									errors.append((folder + filename, folderBack + filename, str(why)))
								except Error, err :
									errors.extend(err.args[0])
								except :
									print "Unexpected error:", sys.exc_info()[0]
						else :
							print "Setting file permissions: " + folderBack + filename
						if not fileerror :
							configBakNew.set("backup", "file" + str(n), filename)
							configBakNew.set("backup", "filedate" + str(n), filedate)
							configBakNew.set("backup", "filemode" + str(n), filemode)
						else :
							n += -1
							files_with_errors += 1
					elif not do_not_follow_links_files :
						try :
							tmp = os.path.realpath(folder + filename)
							if os.path.commonprefix([folderOrg,tmp]) == folderOrg :
								os.symlink(os.path.relpath(tmp, folder), folderBack + filename)
							else :
								os.symlink(tmp, folderBack + filename)
							n += 1
							filedate = os.stat(folder + filename).st_mtime #Read the files last modified date
							filemode = os.stat(folder + filename).st_mode #Read the files permissions
							configBakNew.set("backup", "file" + str(n), filename)
							configBakNew.set("backup", "filedate" + str(n), filedate)
							configBakNew.set("backup", "filemode" + str(n), filemode)
						except (IOError, os.error), why : #catch the Error
							errors.append((folder + filename, os.path.relpath(tmp, folder), str(why)))
						except Error, err :
							errors.extend(err.args[0])
						except :
							print "Unexpected error:", sys.exc_info()[0]
				elif os.path.isdir(folder + filename) :
					if debug :
						print "Object is a directory"
					if recursive == "1" :
						if not os.path.islink(folder + filename) :
							try :
								i = ignore_folders.index(folder + filename + "/")
								ignore_folders.pop(i)
							except ValueError :
								folders.append(folder + filename + "/")
								foldersBack.append(folderBack + filename + "/")
								foldersBackMode.append(folderBack + filename + "/")
								foldersLast.append(folderLast + filename + "/")
						elif not do_not_follow_links_dir :
							tmp = os.path.realpath(folder + filename)
							if os.path.commonprefix([folderOrg,tmp]) == folderOrg :
								if not simulate :
									os.symlink(os.path.relpath(tmp, folder), folderBack + filename)
								if simulate or debug_huge :
									print "Creating symbolic-link (relative): " + os.path.relpath(tmp, folder) + " > " + folderBack + filename
							else :
								if not simulate :
									os.symlink(tmp, folderBack + filename)
								if simulate or debug_huge :
									print "Creating symbolic-link (absolute): " + os.path.relpath(tmp, folder) + " > " + folderBack + filename




################################################################################
##################### WRITE, CURRENT FOLDERS, CONFIG FILE ######################
################################################################################

		configBakNew.set("backup", "files", n)
		configBakNew.set("backup", "source", folder)
		if not simulate :
			if debug_huge :
				print "Writing: " + folderBack + ".backup"
			with open(folderBack + ".backup", "wb") as configfile :
				configBakNew.write(configfile)
		else :
			print "Writing: " + folderBack + ".backup"

################################################################################
############################# FOLDER PERMISSIONS ###############################
################################################################################

while len(foldersBackMode) > 0 : #Continue as long as there is folders left
	folderBackMode = foldersBackMode.pop(0) #Read next folder
	if not simulate :
		if debug_huge :
			print "Setting directory permissions: " + folderBackMode
		try :
			os.chmod(folderBackMode, 365) #Set the read-only permissions
		except (IOError, os.error), why : #catch the Error
			errors.append((folder + filename, folderBack + filename, str(why)))
		except Error, err :
			errors.extend(err.args[0])
		except :
			print "Unexpected error:", sys.exc_info()[0]
	else :
		print "Setting directory permissions: " + folderBackMode

################################################################################
############################## DEBUG INFORMATION ###############################
################################################################################

if debug :
	print "Files hard-linked: " + str(hardlinks)
	print "Files copied: " + str(copies_new) + " (new)"
	print "Files copied: " + str(copies_changed) + " (changed)"
	print "Files with errors: " + str(files_with_errors)

################################################################################
############################## UPDATE folderLast ###############################
################################################################################

maxBakLen += 1
i = range(2, maxBakLen + 1)
i.reverse()
for n in i :
	if config.has_option("backup", "folderLast" + str(n - 1)) :
		config.set("backup", "folderLast" + str(n), config.get("backup", "folderLast" + str(n - 1)))
config.set("backup", "maxBakLen", maxBakLen)
config.set("backup", "folderLast1", folderNow)
config.set("backup", "folderLastCount", 1)

################################################################################
########################## WRITE SOURCE CONFIG FILE ############################
################################################################################

if not simulate :
	if debug_huge :
		print "Writing: " + folderOrg + ".backup"
	with open(folderOrg + ".backup", "wb") as configfile :
		config.write(configfile)
else :
	print "Writing: " + folderOrg + ".backup"

################################################################################
########################## WRITE BACKUP CONFIG FILE ############################ REMOVED, EACH BACKUP CONFIG FILE NOW HAS SOURCE INFORMATION
################################################################################

#configBakNew = ConfigParser.RawConfigParser() #Creating the new config file
#configBakNew.add_section("backup")
#configBakNew.set("backup", "source", folderOrg)
#if not simulate :
#	if debug_huge :
#		print "Writing: " + folderBackOrg + ".backup"
#	with open(folderBackOrg + ".backup", "wb") as configfile :
#		configBakNew.write(configfile)
#else :
#	print "Writing: " + folderBackOrg + ".backup"

################################################################################
############################ CREATE LINK TO LATEST #############################
################################################################################

if not simulate :
	if debug_huge :
		print "Creating link from: " + folderBackOrg + "latest to " + folderBackOrg + folderNow
	if os.path.exists(folderBackOrg + "latest") :
		os.remove(folderBackOrg + "latest")
	os.symlink(folderNow,folderBackOrg + "latest")
else :
	print "Creating link from: " + folderBackOrg + "latest to " + folderBackOrg + folderNow

################################################################################
################################ PRINT ERRORS ##################################
################################################################################

if len(errors) > 0 :
	#WORK NEEDED! Show some kind of error...
	print "ERRORS:"
	print errors

################################################################################
################################# SAY GOODBYE ##################################
################################################################################

if not debug :
	print ""
	print "All done :)"

